package top.wilsonlv.jaguar.cloud.auth.extension.sms;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import top.wilsonlv.jaguar.cloud.auth.properties.JaguarAuthProperties;
import top.wilsonlv.jaguar.cloud.auth.properties.SmsProperties;
import top.wilsonlv.jaguar.cloud.auth.sdk.AuthSdkConstant;
import top.wilsonlv.jaguar.cloud.auth.sdk.sms.SmsCode;
import top.wilsonlv.jaguar.cloud.auth.sdk.sms.SmsType;
import top.wilsonlv.jaguar.cloud.auth.sdk.token.SmsDTO;
import top.wilsonlv.jaguar.cloud.auth.security.UserDetailsServiceImpl;
import top.wilsonlv.jaguar.commons.data.encryption.util.EncryptionUtil;
import top.wilsonlv.jaguar.commons.web.exception.impl.CheckedException;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author lvws
 * @since 2022/3/30 0030
 */
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "jaguar.auth.sms", name = "enable", havingValue = "true")
public class SmsService {

    public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phone";

    public static final String SPRING_SECURITY_FORM_EMAIL_KEY = "email";

    public static final String SPRING_SECURITY_FORM_TICKET_KEY = "ticket";

    public static final String SPRING_SECURITY_FORM_CODE_KEY = "code";


    private final UserDetailsServiceImpl userDetailsService;

    private final JaguarAuthProperties jaguarAuthProperties;

    private final RedisTemplate<String, Serializable> redisTemplate;

    private final VelocityEngine velocityEngine;

    private final MailProperties mailProperties;

    private final JavaMailSender mailSender;


    public SmsCode createSms(String principal, SmsType smsType) {
        SmsProperties smsProperties = jaguarAuthProperties.getSms();

        String phoneZetKey = String.format(AuthSdkConstant.SMS_LOG_ZSET_FORMAT, principal);
        BoundZSetOperations<String, Serializable> zSetOperations = redisTemplate.boundZSetOps(phoneZetKey);

        //1、手机号一分钟内有没有发送过，有则拒绝
        long currentTimestamp = System.currentTimeMillis();
        long startTime = currentTimestamp - smsProperties.getFrequency() * 60000;
        Set<Serializable> smsSendLogs = zSetOperations.reverseRangeByScore(startTime, currentTimestamp);
        if (smsSendLogs != null && smsSendLogs.size() > 0) {
            Double score = zSetOperations.score(smsSendLogs.iterator().next());
            assert score != null;
            long lastSendTime = Double.doubleToLongBits(score);
            throw new CheckedException("操作频繁，请" + ((currentTimestamp - lastSendTime) / 1000) + "秒后再试");
        }

        //2、验证手机号有没有限流，有则拒绝
        startTime = currentTimestamp - smsProperties.getLimitingHour() * 3600000;
        Long count = zSetOperations.count(startTime, System.currentTimeMillis());
        if (count != null && count >= smsProperties.getLimitingCount()) {
            Long expire = zSetOperations.getExpire();
            assert expire != null;
            throw new CheckedException("账号" + principal + "已开启限流，请" + (expire / 60) + "分钟之后再试");
        }

        //3、查询手机号有没有注册，没有则拒绝
        try {
            userDetailsService.loadUserByUsername(principal);
        } catch (UsernameNotFoundException e) {
            throw new CheckedException("账号未注册");
        }

        //4、生成验证码和票据，并发送
        String code = EncryptionUtil.randomCode(0, 0, smsProperties.getCodeLength());
        String ticket = UUID.randomUUID().toString();

        LocalDateTime now = Instant.ofEpochMilli(currentTimestamp).atZone(ZoneId.systemDefault()).toLocalDateTime();
        SmsCode smsCode = new SmsCode();
        smsCode.setPrincipal(principal);
        smsCode.setSmsType(smsType);
        smsCode.setCode(code);
        smsCode.setTicket(ticket);
        smsCode.setCreateTime(now);
        smsCode.setFailNum(0);

        String phoneValueKey = String.format(AuthSdkConstant.SMS_VALUE_FORMAT, principal);
        BoundValueOperations<String, Serializable> valueOperations = redisTemplate.boundValueOps(phoneValueKey);
        valueOperations.set(smsCode, smsProperties.getExpire(), TimeUnit.MINUTES);

        zSetOperations.add(ticket, currentTimestamp);
        zSetOperations.expire(smsProperties.getLimitingHour(), TimeUnit.HOURS);

        log.debug("principal: {}, ticket: {}, code: {}", principal, ticket, code);
        return smsCode;
    }

    @Async
    public void sendSms(SmsCode smsCode) throws IOException, MessagingException {
        Template template = velocityEngine.getTemplate("vm/SmsLogin.vm");
        VelocityContext velocityContext = new VelocityContext();
        velocityContext.put("from", mailProperties.getUsername());
        velocityContext.put("operation", smsCode.getSmsType().getSmsTypeName());
        velocityContext.put("code", smsCode.getCode());

        String content;
        try (StringWriter writer = new StringWriter()) {
            template.merge(velocityContext, writer);
            content = writer.toString();
        }

        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper messageHelper = new MimeMessageHelper(message);
        messageHelper.setFrom(mailProperties.getUsername());
        messageHelper.setTo(smsCode.getPrincipal());
        messageHelper.setSubject("登录授权验证码");
        messageHelper.setText(content, true);
        mailSender.send(message);
    }

    public SmsDTO request2DTO(HttpServletRequest request) {
        boolean isPhone = true;
        String principal = request.getParameter(SPRING_SECURITY_FORM_PHONE_KEY);
        if (StringUtils.hasText(principal)) {
            principal = principal.trim();
        } else {
            principal = request.getParameter(SPRING_SECURITY_FORM_EMAIL_KEY);
            if (StringUtils.hasText(principal)) {
                principal = principal.trim();
                isPhone = false;
            }
        }

        String ticket = request.getParameter(SPRING_SECURITY_FORM_TICKET_KEY);
        if (!StringUtils.hasText(ticket)) {
            throw new BadCredentialsException("ticket为非空");
        }

        String code = request.getParameter(SPRING_SECURITY_FORM_CODE_KEY);
        if (!StringUtils.hasText(code)) {
            throw new BadCredentialsException("短信验证码为非空");
        }

        SmsDTO smsDTO = new SmsDTO();
        smsDTO.setPrincipal(principal);
        smsDTO.setSmsCode(code);
        smsDTO.setTicket(ticket);
        smsDTO.setIsPhone(isPhone);
        return smsDTO;
    }

    public void checkSms(SmsDTO smsDTO) {
        String key = String.format(AuthSdkConstant.SMS_VALUE_FORMAT, smsDTO.getPrincipal());
        BoundValueOperations<String, Serializable> valueOperations = redisTemplate.boundValueOps(key);
        SmsCode smsCode = (SmsCode) valueOperations.get();
        if (smsCode == null) {
            throw new BadCredentialsException("请获取短信验证码");
        }
        if (!smsDTO.getTicket().equals(smsCode.getTicket())) {
            throw new BadCredentialsException("无效的短信验证码票据");
        }

        SmsProperties smsProperties = jaguarAuthProperties.getSms();

        String maxFailMessage = "已达到最大验证次数，请重新获取验证码";
        if (smsCode.getFailNum() >= smsProperties.getMaxFailNum()) {
            throw new BadCredentialsException(maxFailMessage);
        }

        if (!smsDTO.getSmsCode().equals(smsCode.getCode())) {
            smsCode.setFailNum(smsCode.getFailNum() + 1);
            valueOperations.set(smsCode);
            throw new BadCredentialsException("短信验证码错误" +
                    (smsCode.getFailNum().equals(smsProperties.getMaxFailNum()) ? "，" + maxFailMessage : ""));
        }

        if (smsCode.getCreateTime().isBefore(LocalDateTime.now().plusMinutes(-smsProperties.getExpire()))) {
            throw new BadCredentialsException("短信验证码已过期，请重新获取");
        }
    }

}
